# Copyright (C) 1999--2002  Joel Rosdahl
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
#
# Joel Rosdahl <joel@rosdahl.net>

"""ircbot -- Simple IRC bot library.

This module contains a single-server IRC bot class that can be used to write
simple bots and extensive subclasses."""

import sys
from irclib import SimpleIRCClient
from irclib import IRCChannel, IRCUser
from irclib import nm_to_n, irc_lower, all_events
from irclib import parse_channel_modes, is_channel
from irclib import ServerConnectionError
from irclib import VERSION

class SingleServerIRCBot(SimpleIRCClient):
	"""A single-server IRC bot class.
	
	The bot tries to reconnect if it is disconnected.
	
	The bot keeps track of the channels it has joined, the other clients that
	are present in the channels and which of those that have operator or voice
	modes. The "database" is kept in the self.channels attribute, which is an
	`IRCDict` of `IRCChannel` objects."""
	
	def __init__(self):
		"""Constructor for SingleServerIRCBot objects.
		
		Arguments:
		 - server_list: A list of tuples (server, port) that defines which
		   servers the bot should try to connect to.
		 - nickname: The bot's nickname.
		 - realname: The bot's realname.
		 - reconnection_interval: How long the bot should wait before trying to
		   reconnect.
		 - dcc_connections: A list of initiated/accepted DCC connections.
		
		Also defines a few global handlers for events."""
		
		SimpleIRCClient.__init__(self)
		self.channels = IRCDict()
		self.server_list = server_list
		if not reconnection_interval or reconnection_interval < 0:
			reconnection_interval = 2**31
		self.reconnection_interval = reconnection_interval
		
		self._nickname = nickname
		self._realname = realname
		for i in ["disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit"]:
			self.connection.add_global_handler(i, getattr(self, "_on_" + i), -10)
	
	def _connected_checker(self):
		"""Checks whether the connection is still open"""
		
		if not self.connection.is_connected():
			self.connection.execute_delayed(self.reconnection_interval, self._connected_checker)
			self.jump_server()
	
	def _connect(self):
		"""Attempts to connect to the server."""
		
		password = None
		if len(self.server_list[0]) > 2:
			password = self.server_list[0][2]
		try:
			self.connect(self.server_list[0][0], self.server_list[0][1], self._nickname, password, ircname=self._realname)
		except ServerConnectionError:
			pass
	
	def _on_disconnect(self, c, e):
		"""Attempts to reconnect to the server after a forced disconnect."""
		
		self.channels = IRCDict()
		self.connection.execute_delayed(self.reconnection_interval, self._connected_checker)
	
	def _on_join(self, c, e):
		"""Creates an initial `Channel` object if the bot just joined the 
		channel, or adds the user that just joined to the channel list."""
		
		if e.source.nick == c.real_nickname or e.target not in self.channels:
			self.channels[e.target] = IRCChannel(c)
		
		self.channels[e.target].add_user(e.source)
	
	def _on_kick(self, c, e):
		"""Removes the user from the channel list, or deletes the `Channel`
		object if the bot was kicked."""
		
		nick = e.arguments[0]
		
		if nick == c.real_nickname:
			del self.channels[e.target]
		elif e.target in self.channels:
			self.channels[e.target].remove_user(nick)
	
	def _on_mode(self, c, e):
		"""Parses channel modes, and sets or clears the modes from the `Channel`
		object."""
		
		modes = parse_channel_modes(" ".join(e.arguments()))
		t = e.target()
		if is_channel(e.target):
			ch = self.channels[t]
			for mode in modes:
				if mode[0] == "+":
					f = ch.set_mode
				else:
					f = ch.clear_mode
				f(mode[1], mode[2])
		else:
			# Mode on self... XXX Shouldn't happen?
			pass
	
	def _on_namreply(self, c, e):
		"""Processes a reply to a WHO request."""
		
		# Contents of e.arguments():
		# - [0]:
		#   - "@" for secret channels
		#   - "*" for private channels
		#	- "=" for others (public channels)
		# - [1]: channel
		# - [2]: string of space-separated nicknames with modes
		
		ch = e.arguments[1]
		for nick in e.arguments[2].split():
			if nick[0] == "@":
				nick = nick[1:]
				self.channels[ch].set_mode("o", nick)
			elif nick[0] == "+":
				nick = nick[1:]
				self.channels[ch].set_mode("v", nick)
			
			self.channels[ch].add_user(nick)
	
	def _on_nick(self, c, e):
		"""Changes the nickname of the user in every `Channel` list they're
		on."""
		
		before = e.source.nick
		after = e.target
		for ch in self.channels.values():
			if ch.has_user(before):
				ch.change_nick(before, after)
	
	def _on_part(self, c, e):
		"""Removes the user from the channel list, or deletes the `Channel`
		object if the bot left."""
		
		if e.source.nick == c.real_nickname:
			del self.channels[e.target]
		else:
			self.channels[e.target].remove_user(e.source)
	
	def _on_quit(self, c, e):
		"""Removes the user from every `Channel` list they were on."""
		
		for ch in self.channels.values():
			if ch.has_user(e.source):
				ch.remove_user(e.source)
	
	def die(self, msg="Bye, cruel world!"):
		"""Lets the bot die. A quit message can be used on disconnect."""
		
		# TODO: The message doesn't always get through (socket closes early)
		self.connection.disconnect(msg)
		sys.exit(0)
	
	def disconnect(self, msg="I'll be back!"):
		"""Disconnects the bot. The bot may try to reconnect after a while.
		A quit message can be used on disconnect."""
		
		self.connection.disconnect(msg)
	
	def get_version(self):
		"""Returns the bot version.
		
		Used when answering a CTCP VERSION request."""
		
		return("ircbot.py with irclib/" + ".".join([str(v) for v in VERSION]))
	
	def jump_server(self, msg="Changing servers"):
		"""Connect to a new server, possibly disconnecting from the current.
		
		The bot will skip to next server in the server_list each time
		jump_server is called.
		"""
		if self.connection.is_connected():
			self.connection.disconnect(msg)
		
		self.server_list.append(self.server_list.pop(0))
		self._connect()
	
	def on_ctcp(self, c, e):
		"""Default handler for ctcp events.
		
		Replies to VERSION and PING requests and relays DCC requests
		to the on_dccchat method.
		
		TODO: Move botbot's code into here."""
		
		if e.arguments[0] == "VERSION":
			c.ctcp_reply(e.source.nick, "VERSION " + self.get_version())
		elif e.arguments[0] == "PING":
			if len(e.arguments) > 1:
				c.ctcp_reply(e.source.nick, "PING " + e.arguments[1])
		elif e.arguments[0] == "DCC" and e.arguments[1].split(" ", 1)[0] == "CHAT":
			self.on_dccchat(c, e)
	
	def on_dccchat(self, c, e):
		pass
	
	def start(self):
		"""Starts the bot."""
		
		self._connect()
		SimpleIRCClient.start(self)
